NIO 基础

NIO 和 IO 的区别

IO 是以流的方式进行数据传输,它是同步阻塞类型。我们可以将流看成生活中的水流,它是单向的。
NIO 是以通道的方式进行数据传输,它是同步非阻塞类型。我们可以将通道看成铁道,通道是不存储数据的,它采用buffer来存储数据,由通道运输到两端。它是一个双向的。

缓冲区的数据存取

基础类型中,除了boolean没有buffer以外,NIO给每个类型都提供了相应的buffer.
java.nio.Buffer;
|– ByteBuffer
|– CharBuffer
|– IntBuffer
|– ShortBuffer
其中,buffer 提供了一些方法,用于存储和输出数据,以ByteBuffer为例:

1
2
3
4
5
6
7
8
9
10
11
//1. 实例化一个buffer,大小为1024字节
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//2.取数据
byteBuffer.get();
//3.存数据
byteBuffer.put();
//4.rewind(); 可重复读

//5.clear(); 回到最初状态,但是数据不清空

//6.mark(); 标记 搭配 reset();重复 使用

四个核心属性:

  • capacity:容量,表示缓冲区中最大存储数据容量,一但声明,不允许改变。
  • limit:界限,表示缓冲区中可操作数据大小。(limit 后数据不能进行读写)
  • position:位置,表示缓冲区中正在操作数据的位置。
  • positon <= limit <= capacity

直接缓冲区和非直接缓冲区

  • 非直接缓冲区:通过allocate() 方法分配缓冲区,在JVM内存中
  • 直接缓冲区:通过 allocateDirect() 分配直接缓冲区。建立在物理内存中

管道channel

用于源节点和目标节点的连接。在java nio 中负责缓冲区中的数据传输。类似于流,但是不能访问数据,只能与Buffer进行交互。

DMA:直接存储器

最开始,由CPU进行IO接口请求处理,启用多个线程。如果没有数据,则该线程则进入等待状态。后又采用DMA总线方式控制线程。但DMA还是需要向CPU请求资源,后将DMA替换成Channel。一个完全独立的处理器,用来处理IO.无需向CPU申请。

  • Java.nio.channels.channel 接口:
    • FileChannel
    • SocketChannel
    • ServerSocketChannel
    • DatagranChannel
  • 获取通道 getChannel
    • 本地IO
      • FileInputStream / FileOutputStream
      • RandomAccessFile
    • 网络IO
      • Socket
      • ServerSocket
      • DatagramSocket
    • JDK 1.7
      • open
      • Files工具类的newByteChannel()

通道数据传输和内存映射文件

 

  • 以下是四大文件操作对比:
  • 通道(Channel)的数据传输(采用非直接缓冲区)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testChannel() throws IOException {
FileInputStream fileInputStream = new FileInputStream("Java NIO.pdf");
FileOutputStream fileOutputStream = new FileOutputStream("2.pdf");

// 1、获取通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();

// 2.分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

// 3、将通道的数据读入缓冲区
while (inChannel.read(byteBuffer) != -1) {
byteBuffer.flip();// 切换缓冲区为读模式
// 4、把缓冲区的数据写入通道
outChannel.write(byteBuffer);
byteBuffer.clear();// 因为需要循环多次读,需要清空缓冲区。
}

byteBuffer.clear();
inChannel.close();
outChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
  • 内存映射文件(采用直接缓冲区)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testMemoryMappingFile() throws IOException {
long start = System.currentTimeMillis();

FileChannel inChannel = FileChannel.open(Paths.get("D:\\nio.zip"), StandardOpenOption.READ);
// 注意:StandardOpenOption.CREATE
// 如果文件已经存在,直接覆盖,StandardOpenOption.CREATE_NEW,如果文件已经存在,就抛出异常。
FileChannel outChannel = FileChannel.open(Paths.get("E:\\nio.zip"), StandardOpenOption.READ,
StandardOpenOption.WRITE, StandardOpenOption.CREATE);

// 获取内存映射文件
MappedByteBuffer inMappedByteBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedByteBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());

// 直接对数据进行读写
byte[] bytes = new byte[inMappedByteBuffer.limit()]; // 此时,如果数据读超出了一定返回会抛出异常。如果内存不足时,会抛出java.lang.OutOfMemoryError: Java heap space
inMappedByteBuffer.get(bytes);
outMappedByteBuffer.put(bytes);

inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();

System.out.println((end - start));
}
  • transferTo&transferFrom将数据从源通道传输到其他 Channel 中(采用直接缓存区)
1
2
3
4
5
6
FileChannel inChannel = FileChannel.open(Paths.get("D:\\nio.zip"), StandardOpenOption.READ);
// 注意:StandardOpenOption.CREATE
// 如果文件已经存在,直接覆盖,StandardOpenOption.CREATE_NEW,如果文件已经存在,就抛出异常。
FileChannel outChannel = FileChannel.open(Paths.get("E:\\nio.zip"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());

分散读取和聚集写入

分散读取:将通道中的数据分散到多个缓冲区中

1
2
3
4
5
6
7
//分配制定大小缓冲区
ByteBuffer buffer1 = ByteBuffer.allocate(100);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
//分散读取
ByteBuffer[] buffers = {buffer1,buffer2};
//通道里面进行读取
channel.read(buffers);

聚集写入:将多个缓冲区中的数据集中到通道中

1
2
3
4
5
6
7
8
9
//写入完毕之后 for 循环
for(ByteBuffer byteBuffer : buffers) {
//切换成读的模式
byteBuffer.flip();
}
RandomAccessFile randomAccessFile = new RandomAccessFile("test2.txt", "rw");
//获取通道
FileChannel channel2 = randomAccessFile.getChannel();
channel2.write(buffers);

字符集Charset

在java.nio.charset包中共提供了Charset。向ByteBuffer中存放数据时需要考虑字符集的编码方式,从中读取时需要考虑字符集的解码。要读和写文本需要分别使用CharsetDecoder(解码器)和CharsetEncoder(编码器)。
在JDK源码中提供如下静态方法得到一个CharSet实例:

1
CharSet cs = CharSet.forName(“编码方式”);

得到一个CharSet实例后,我们需要创建一个编码器和一个解码器,使用下面方法进行创建:

1
2
3
CharSetDecoder decoder = cs.newDecoder();

CharSetEncoder encoder = cs.newEncoder();

接着我们把ByteBuffer传递给decoder进行编码,返回一个CharBuffer:

1
CharBuffer cb = decoder.decode(inputData);

然后我们可以使用encoder进行解码返回一个ByteBuffer:

1
ByteBuffer outputData = encoder.encode(cb);

阻塞模式和非阻塞模式

阻塞模式:

  • 将直接使用accept()监听事件

非阻塞模式:

  • 采用选择器,将通道注册到选择器上, 并指定监听接收事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/** client */
//1.获取通道
SockectChannel schannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
//2.切换非阻塞模式
schanne.configureBloking(false);
//3.配置Buffer ...
ByteBuffer buf = ByteBuffer.allocate(1024);
//4.发送数据给服务器
Scanner scan = new Scanner(System.in);

while(scan.hasNext()){
String str = scan.next();
buf.put(str.getByte());
buf.flip();
schannel.write(buf);
buf.clear();
}


/** server */
//1.通道获取
ServerSocketChannel sschannel = ServerSocketChannel.open();
//2.切换非阻塞模式
sschannel.configureBloking(false);
//3.绑定连接
sschannel.bind(new InetSocketAdderss(9899));
//4.获取选择器
Selector selector = Serlector.open();
//5.将通道注册到选择器上, 并指定监听接收事件
sschannel.register(selector,SelectionKey.OP_ACCEPT);
//6.轮询式的获取选择器上已经就绪的事件
while(sockect.select() > 0){
//7.获取当前选择器中所有组测的选择键(已就绪的监听事件)
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
//8.获取准备就绪事件
SelectionKey sk = it.next();
//9.判断具体就绪事件类型
if(sk.isAcceptable()){
//10.若接收就绪,获取客户端连接
SockectChannel schannel = schannel.accpet();
//11.切换非阻塞模式
schannel.configueBloking(false);
//12.将通道注册到选择器
schannel.register(selector,SelectionKey.OP_READ);
}else if(sk.isReadable()){
//读事件处理
}
//移除 SelectionKey
it.remove();
}
}

`

管道(pipe)

Java NIO 管道式两个线程之间的单向数据连接。

Pipe 有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
//1.获取管道
Pipe pipe = Pipe.open();
//2.将缓冲区中的数据写入管道
ByteBuffer buf = ByteBuffer.allocate(1024);

Pipe.SinkChannel sinkChannel = pipe.sink();
buf.put("通过担心管道发送数据".getBytes());
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(),0,len));

sourceChannel.close();
sinkChannel.close();